/*
 * OpenGL tools - Shadow mapping example.
 * 
 * MIT License
 * 
 * Copyright (c) 2009, Markus Broecker <mbrckr@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <OpenGL.h>
#include <Framebuffer.h>
#include <Shader.h>

#include <cstdlib>
#include <cmath>

using namespace glTools;

static int gWindow = 0;
static unsigned int gTimestamp = 0;

static Depthbuffer* gDepthmap = 0;
static Shader*		gShadowShader = 0;

static bool			gRenderDepthmap = false;


struct Light
{
	struct
	{
		float x, y, z;
	} position, at, up;
	
	float animation, animationSpeed;

	float fov, ratio, near, far;

	

	inline Light() : animation(0.f), animationSpeed(1.5f), 
					fov(45.f), ratio(1.f), near(5.f), far(100.f)
	{
		position.x = 10;
		position.y = 10;
		position.z = 10;
		
		at.x = 0.f;
		at.y = 0.f;
		at.z = 0.f;
		
		up.x = 0.f;
		up.y = 1.f;
		up.z = 0.f;
	}
	
	inline void update(float delta)
	{
		// Update the position of the light
		animation += delta * animationSpeed;
		position.x = cos(animation) * 20.0;
		position.y = 10.0;
		position.z = sin(animation) * 20.0;
	}

};

Light	gLight0;

static void renderScene()
{
	// render a ground plane
	glColor3f(0.5, 0.5, 0.5);
	glNormal3f(0.f, 1.f, 0.f);
	glBegin(GL_QUADS);
	glVertex3i(-20, -2, -20);
	glVertex3i(-20, -2, 20);
	glVertex3i(20, -2, 20);
	glVertex3i(20, -2, -20);
	glEnd();
	
	
	glColor3f(0.8f, 0.3f, 0.f);
	/*
	glFrontFace(GL_CW);
	glutSolidTeapot(4.f);
	glFrontFace(GL_CCW);
	*/
	glutSolidCube(4.0);
}

static void renderDepthMap()
{
	// bind and clear the depth FBO
	gDepthmap->bind();
	glClear(GL_DEPTH_BUFFER_BIT);
	
	// setup the light source
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(gLight0.fov, gLight0.ratio, gLight0.near, gLight0.far);
	
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(gLight0.position.x, gLight0.position.y, gLight0.position.z, 0, 0, 0, 0, 1, 0);
	
	// disable front face rendering and store the scene in the depth FBO
	glCullFace(GL_FRONT);
	renderScene();
	glCullFace(GL_BACK);
	
	gDepthmap->disable();
}

static void renderShadowedScene()
{
	// Setup the 'camera'
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, 1.0, 1.0, 100.0);
	
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(10, 10, 10, 0, 0, 0, 0, 1, 0);
	
	gShadowShader->bind();
	gShadowShader->setTexture2D("depthmap", gDepthmap->getBuffer());
	
	/* 	assume that texture matrix7 has been loaded with the correct view/proj
		matrix.
	*/
	
	renderScene();
	
	
	gShadowShader->disable();
	
}

static void setupTextureMatrix()
{
	glActiveTexture(GL_TEXTURE7);
	glMatrixMode(GL_TEXTURE);
	
	static const float bias[] ={0.5f, 0.f, 0.f, 0.f, 
								0.f, 0.5f, 0.f, 0.f, 
								0.f, 0.f, 0.5f, 0.f,
								0.5f, 0.5f, 0.5f, 1.f};
	glLoadMatrixf(bias);
	gluPerspective(gLight0.fov, gLight0.ratio, gLight0.near, gLight0.far);
	gluLookAt(gLight0.position.x, gLight0.position.y, gLight0.position.z, gLight0.at.x, gLight0.at.y, gLight0.at.z, gLight0.up.x, gLight0.up.y, gLight0.up.z);

	glActiveTexture(GL_TEXTURE0);
	glMatrixMode(GL_MODELVIEW);
}

// renders the depth map for debugging purposes
static void renderDepthTexture()
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, 1, 0, 1, 0, 1);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, gDepthmap->getBuffer());

	glColor3f(1.f, 1.f, 1.f);
	glBegin(GL_QUADS);
	glTexCoord2i(0, 1);
	glVertex2i(0, 1);
	glTexCoord2i(0, 0);
	glVertex2i(0, 0);
	glTexCoord2i(1, 0);
	glVertex2i(1, 0);
	glTexCoord2i(1, 1);
	glVertex2i(1, 1);
	glEnd();
	
	glDisable(GL_TEXTURE_2D);
}

static void display()
{
	renderDepthMap();

	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
	if (gRenderDepthmap)
		renderDepthTexture();
	else
	{
		setupTextureMatrix();
		renderShadowedScene();
	}
	
	glutSwapBuffers();
	
	glError();	
}

static void keyboard(unsigned char key, int x, int y)
{
	// exit on escape
	if (key == 27)
		exit(0);
	
	if (key == ' ')
		gRenderDepthmap = !gRenderDepthmap;

}

static void idle()
{
	unsigned int time = glutGet(GLUT_ELAPSED_TIME);
	float delta = static_cast<float>(time - gTimestamp) / 1000.f;
	gTimestamp = time;

	gLight0.update(delta);

	glutPostRedisplay();
}

int main(int argc, const char** argv)
{
	glutInit(&argc, const_cast<char**>(argv));
	glutInitDisplayMode(GLUT_RGBA|GLUT_DEPTH|GLUT_DOUBLE);
	glutInitWindowSize(512, 512);
	
	gWindow = glutCreateWindow("shadow mapping");
	
	#ifndef __APPLE__
		glewInit();
	#endif
	
	gDepthmap = new Depthbuffer(512, 512);
	gShadowShader = new Shader("./shadowmapping.vert", "./shadowmapping.frag");
	
	glutDisplayFunc(display);
	glutIdleFunc(idle);
	glutKeyboardFunc(keyboard);
	
	
	gLight0.animation = 0.0;
	
	glClearColor(0.5, 0, 0, 0);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glFrontFace(GL_CCW);
	
	
	glutMainLoop();
	
	return 0;
}